13장. 균형 잡힌 결합의 실제
출처: 『소프트웨어 설계의 결합 균형』(블라드 코노노프 지음, 장연호 옮김, 제이펍 2026) | 공식: https://www.jpub.kr/
앞 12개 장에서 쌓은 결합 원리(통합 강도·거리·변동성·균형)를 8개 실제 시나리오에 적용한다. 마이크로서비스부터 단일 메서드까지 같은 균형 결합 원리가 프랙털처럼 반복됨을 확인한다.
학습 목표
이 장을 끝내면 다음을 할 수 있다.
- 이벤트 소싱에서 비공개/공개 이벤트를 분리해야 하는 이유를 결합 세 차원으로 설명한다.
- 변동성이 낮으면 모델 결합도 "충분히 좋은 통합"이 될 수 있음을 판단한다.
- 계층 아키텍처 vs 수직 슬라이스 vs 포트·어댑터의 결합 균형 차이를 비교한다.
- 집계(Aggregate) 패턴이 트랜잭션 경계와 거리를 어떻게 정렬하는지 분석한다.
- 폴더 구성을 기술 역할에서 기능 책임으로 바꾸는 설계 판단을 적용한다.
- 모델 결합을 계약 결합으로 낮추는 메서드 수준 리팩터링을 수행한다.
전체 흐름도
8개 사례를 결합 균형 세 차원·추상화 수준으로 매핑:
추상화 수준 사례 핵심 전환
─────────────────────────────────────────────────────────
마이크로서비스 사례1 (SCM-Autopilot) 모델 결합 → 계약 결합
사례2 (Desks-Distribution) 현상 유지 (충분히 좋은 통합)
─────────────────────────────────────────────────────────
서비스 내 모듈 사례3 (계층 → 수직 슬라이스) 기능 결합 해소
사례4 (포트·어댑터) 우발적 변동성 제거
─────────────────────────────────────────────────────────
클래스 사례5 (집계) 트랜잭션 경계 명확화
사례6 (폴더 구성) 기술 기준 → 기능 기준
─────────────────────────────────────────────────────────
메서드 사례7 (ISP — 분할 정복) 공유 지식 없으면 거리 늘리기
사례8 (코드 스멜 — SLA) 모델 결합 → 도메인 서비스
─────────────────────────────────────────────────────────
공통 질문: "강도↓·거리↑" 이어야 할 곳을 찾아 분리하고,
"강도↑·거리↓" 이어야 할 곳을 찾아 묶는다.
0. 사전 필수 용어
| 용어 | 설명 |
|---|---|
| 통합 강도 | 두 구성요소가 공유하는 지식의 양. 낮을수록 변경 전파 감소. (7장) |
| 거리 | 두 구성요소가 서로 얼마나 가까이 위치하는가. 파일·모듈·서비스 수준으로 달라짐. (8장) |
| 변동성 | 구성요소가 얼마나 자주 바뀌는가. 핵심 하위 도메인 = 높음, 지원 하위 도메인 = 낮음. (9장) |
| 균형 잡힌 결합 | 높은 통합 강도는 짧은 거리와, 낮은 통합 강도는 먼 거리와 짝짓는 원칙. |
| 모델 결합 | 소비자가 공급자의 내부 도메인 모델 구조를 알아야 하는 결합. |
| 계약 결합 | 소비자가 명시적 계약(인터페이스·공개 이벤트 스키마)만 알면 되는 결합. |
| 이벤트 소싱 | 상태 변화를 이벤트 연속으로 기록하는 패턴. 현재 상태 = 이벤트 재생 계산. |
| 경계 맥락 | 도메인 모델이 유효하게 통용되는 명시적 경계. 마이크로서비스 ≈ 경계 맥락. |
| 집계 | 동일한 트랜잭션 경계를 공유하는 개체 클러스터. 집계 루트를 통해서만 수정. |
| 수직 슬라이스 아키텍처 | 조직 원칙을 기술 계층이 아닌 비즈니스 기능으로 삼는 아키텍처. |
| 포트·어댑터 | 도메인을 인프라와 분리. 인터페이스(포트) + 구체 구현(어댑터). 육각형 아키텍처. |
| 우발적 변동성 | 외부 의존성 때문에 원래 안정적이어야 할 구성요소가 변동성을 갖게 되는 현상. |
| 도메인 서비스 | 특정 개체에 귀속하기 어려운 알고리즘을 담당하는 논리적 클래스. |
| 공개/비공개 이벤트 | 공개 = 외부 통합 전용 최소 지식 이벤트. 비공개 = 내부 수명주기 모델링용. |
| WolfDesk | 이 책 전체의 가상 헬프 데스크 시스템. SCM·Desks·Distribution·Autopilot 등으로 구성. |
1. 사례 연구 1 — 이벤트 소싱: 외부 지식 최소화
문제 상황
SCM(Support Case Management)은 이벤트 소싱으로 구현된 핵심 하위 도메인이다. 팀이 내부 이벤트 전부를 메시지 버스에 게시하고 Support Autopilot이 구독해 ML 모델을 학습시켰다.
// 내부 이벤트 예시 (일부)
{ "eventType": "CaseCreated", "caseId": "CASE2101", "customerId": "CUST52" }
{ "eventType": "CaseAssigned", "caseId": "CASE2101", "assignedTo": "AGNT007" }
{ "eventType": "CaseEscalated", "caseId": "CASE2101", "escalationLevel": "Level 2" }
{ "eventType": "CaseResolved", "caseId": "CASE2101" }
{ "eventType": "CaseReopened", "caseId": "CASE2101", "reason": "..." }
시간이 지나자 SCM이 이벤트 스키마를 변경할 때마다 Autopilot 팀과 협의가 필요해져 변경 속도가 느려지고 팀 간 마찰이 커졌다.
결합 관점 분석
SCM은 핵심 하위 도메인 → 변동성 높음. 두 서비스는 다른 팀 → 거리 멀다. 내부 이벤트를 그대로 공유 → 모델 결합.
[ 잘못된 예 ]
SCM ─── (모든 내부 이벤트) ─────────────────────────► Autopilot
모델 결합 + 높은 변동성 + 먼 거리 = 마찰 최대
[ 올바른 예 ]
SCM → [비공개 이벤트: 내부 수명주기] → (내부 변환) → [공개 이벤트] → Autopilot
계약 결합 + 안정 스키마
해결: 공개 이벤트 분리
// 통합 특화 공개 이벤트 (단일 스냅숏)
{
"caseId": "CASE2101",
"caseVersion": 10,
"createdOn": "20230704T09:00:00Z",
"lastModifiedOn": "20230713T16:00:00Z",
"customerId": "CUST52",
"messages": [...],
"status": "RESOLVED",
"wasReopened": true,
"isEscalated": true,
"agent": "AGNT009",
"prevAgents": ["AGNT007"]
}
- 비공개 이벤트: 내부 수명주기 모델링에만 사용.
- 공개 이벤트: 외부 통합에 필요한 최소 지식만 담은 단일 스냅숏.
결과
통합 강도: 모델 결합 → 계약 결합. 높은 변동성과 먼 거리를 효과적으로 균형 잡음.
2. 사례 연구 2 — 충분히 좋은 통합
문제 상황
Desks(일정 관리)는 일정 변경 이벤트를 게시하고, Distribution(에이전트 배정)이 구독한다. Desks가 내부 모델 그대로 이벤트를 노출 → 모델 결합. 구조는 사례 1과 동일하다.
그런데 문제가 없었다
Desks는 지원 하위 도메인 → 변동성 낮음. 내부 모델이 자주 바뀌지 않으니 모델 결합이어도 실제 변경 전파 빈도가 낮다.
비교 분석
| 항목 | 사례 1 (SCM) | 사례 2 (Desks) |
|---|---|---|
| 하위 도메인 분류 | 핵심 (변동성 높음) | 지원 (변동성 낮음) |
| 서비스 간 거리 | 멀다 | 멀다 |
| 통합 강도 | 모델 결합 | 모델 결합 |
| 실제 마찰 | 심각 | 없음 |
| 해결 방향 | 공개 이벤트 분리 필수 | 현상 유지 (충분히 좋은 통합) |
핵심 교훈
같은 통합 강도·같은 거리라도 변동성에 따라 판단이 달라진다. 균형 잡힌 결합은 세 차원(강도·거리·변동성)을 함께 읽는 것이다. 변동성이 낮으면 엄격한 계약 설계 비용이 얻는 이점보다 클 수 있다.
3. 사례 연구 3 — 계층 아키텍처의 복잡성과 수직 슬라이스
문제 상황
SCM 초기 설계는 4계층 아키텍처(프레젠테이션 → 애플리케이션 → 비즈니스 로직 → 데이터 액세스)였다. 두 가지 문제가 생겼다.
- 전역 복잡성: 거의 모든 기능 변경에 4개 계층 모두 수정 필요 → 계층 간 기능 결합.
- 지역 복잡성: 같은 데이터 액세스 계층 안의 CustomerRepository·AgentRepository·CaseRepository는 서로 가까이 있지만 동시에 바뀔 일이 거의 없음.
[ 계층 아키텍처의 균형 불량 ]
계층 간: 높은 통합 강도 + 먼 거리 = 전역 복잡성 (불균형)
계층 내: 낮은 통합 강도 + 가까운 거리 = 지역 복잡성 (불균형)
해결: 수직 슬라이스 아키텍처
조직 원칙을 기술 계층 → 비즈니스 기능으로 전환.
[ 수직 슬라이스 결과 ]
슬라이스 간: 낮은 통합 강도 + 먼 거리 → 균형
슬라이스 내: 높은 통합 강도(기능 결합) + 가까운 거리 → 균형
각 슬라이스 안에서 프레젠테이션·애플리케이션·비즈니스 로직·데이터 액세스가 공존하고, 슬라이스 간에는 관계가 거의 없다. 수직 슬라이스는 새로운 추상화 수준을 만들어 "기능 관점"으로 서비스 구성요소를 추론할 수 있게 한다.
4. 사례 연구 4 — 포트·어댑터로 우발적 변동성 제거
문제 상황
수직 슬라이스 전환 후에도 지원사례수명주기 슬라이스에서 변경 시 문제가 발생했다. 계층 간 종속성이 위→아래 방향이라 비즈니스 로직 계층이 데이터 액세스 계층의 내부 설계 결정(DB 컬럼, 쿼리 방식)을 알게 됐다. 이것이 기능 결합이며, 핵심 도메인 로직 구현과 테스트를 어렵게 만들었다.
해결: 포트·어댑터 (종속성 역전)
[ 계층 아키텍처 — 종속성 방향 ]
비즈니스로직 ──▶ 데이터액세스 (도메인이 인프라를 앎 — 문제)
[ 포트·어댑터 — 종속성 역전 ]
인프라 ──▶ 애플리케이션 ──▶ 도메인 (인프라가 도메인에 맞춤 — 해결)
// 도메인 계층에 포트(인터페이스) 정의
namespace WolfDesk.SCM.CustomerPortal.Domain {
public interface IProductRepository {
Product Load(ProductId id);
void Update(Product product);
IEnumerable<Product> FindAll();
IEnumerable<Product> FindByStatus(ProductStatus status);
}
}
// 인프라 계층에 어댑터(구체 구현) 위치
namespace WolfDesk.SCM.CustomerPortal.Infrastructure.DB {
class PostgresProductRepository : IProductRepository {
// 도메인 계층은 PostgreSQL이라는 사실을 모른다
}
}
두 가지 효과
- 지식 단방향화: 인프라 → 도메인으로 지식이 흐르지 않는다. 비즈니스 로직을 인프라 없이 추론·테스트할 수 있다.
- 우발적 변동성 제거: 도메인 계층이 변동성 높은 인프라에 종속되면 도메인도 변동성을 갖게 된다. 포트·어댑터는 이 우발적 변동성을 차단한다.
결과
계층 간 통합 강도: 기능 결합 → 모델 결합 또는 계약 결합으로 감소. 먼 거리 + 낮은 강도 = 모듈성 증가.
5. 사례 연구 5 — 집계 패턴으로 트랜잭션 경계 명확화
문제 상황
초기에 SupportCase·Message·Customer·Agent 4개 클래스를 ORM으로 연결하고 양방향 참조를 허용했다.
// 잘못된 예: 양방향 참조 — 트랜잭션 경계 불명확
class SupportCase {
private Customer openedBy; // 객체 직접 참조
private Agent assignedAgent; // 객체 직접 참조
private List<Message> messages;
}
class Customer {
private List<SupportCase> openedCases; // 역방향 참조
}
class Agent {
private List<SupportCase> assignedCases;
}
두 가지 문제:
- 성능: 객체 탐색 남용(사례 → 고객 → 같은 고객의 다른 모든 사례 로드). 불필요한 데이터 대량 조회.
- 트랜잭션 경계 미강제: 사례 확대 + 새 메시지 추가는 원자 트랜잭션이어야 하지만 설계가 이를 강제하지 않아, 두 커밋 사이에 에이전트가 응답하면 사례가 잘못 확대되는 버그 발생.
결합 관점 분석
| 클래스 쌍 | 트랜잭션 결합 필요? | 설계 결정 |
|---|---|---|
| SupportCase - Message | 필요 (원자적 변경) | 같은 집계 (직접 참조) |
| SupportCase - Customer | 불필요 | ID 참조만 (거리 확대) |
| SupportCase - Agent | 불필요 | ID 참조만 (거리 확대) |
해결: 집계 패턴
// 올바른 예: 집계 패턴
class SupportCase {
private CustomerId openedBy; // 객체 아닌 ID (거리 명시적 확대)
private AgentId assignedAgent; // 객체 아닌 ID
private List<Message> messages; // 직접 참조 (같은 집계)
}
// 잘못된 예: 2단계 커밋 (원자성 없음)
var sCase = repo.Load(caseId);
sCase.Escalate();
repo.Save(sCase); // 1차 커밋
// ← 이 사이에 에이전트가 응답하면?
sCase.AddMessage(newMsg);
repo.Save(sCase); // 2차 커밋 (버그 가능)
// 올바른 예: 집계 내 원자적 변경
var sCase = repo.Load(caseId); // SupportCase + Messages 함께 로드
sCase.TrackCustomerEmail(email, departments); // 내부에서 확대 + 메시지 함께 처리
repo.Save(sCase); // 단일 트랜잭션 커밋
6. 사례 연구 6 — 폴더 구성: 기술 역할 → 기능 책임
문제 상황
파일이 늘자 팀이 기술 패턴별 폴더를 만들었다.
// 잘못된 예: 기술 역할 기준 폴더
WolfDesk/SupportCaseManagement/SupportCases/Domain/
Entities/ SupportCase.cs, Message.cs, Priority.cs, Status.cs ...
Events/ CaseInitialized.cs, MessageReceived.cs, CaseResolved.cs ...
Factories/ SupportCaseFactory.cs, MessageFactory.cs
Repositories/ ISupportCaseRepository.cs
Message를 변경하려면 Entities·Events·Factories 세 폴더를 모두 열어야 한다. 가까이 있는 파일이 함께 바뀌지 않고, 함께 바뀌는 파일이 다른 폴더에 흩어진다. 사례 3(계층 아키텍처)과 동일한 문제.
해결: 기능 책임 기준 폴더
// 올바른 예: 기능 기준 폴더
WolfDesk/Domain/SupportCases/
CaseInitialized.cs
CaseResolved.cs
CaseReopened.cs
ISupportCaseRepository.cs
SupportCase.cs
Status.cs
Priority.cs
/Messages/
Message.cs
MessageBody.cs
MessageFactory.cs
MessageReceived.cs
Recipient.cs
기능 변경 시 열어야 할 폴더가 한 곳으로 줄어든다. 지역·전역 복잡성 모두 감소, 인지 부하 감소.
7. 사례 연구 7 — 분할 정복: ISP 적용
문제 상황
SupportCase 클래스에 인프라 관심사(알림 메서드)가 섞여 있었다.
// 잘못된 예: 단일 책임 원칙 위반
public class SupportCase {
public void CreateCase(...) { ... }
public void AssignAgent(...) { ... }
public void SendEmailNotification(...) { ... } // 인프라 관심사
public void SendSMSNotification(...) { ... } // 인프라 관심사
}
포트·어댑터 전환 후 알림을 인프라 계층으로 이동했다. 이제 SendEmail과 SendSMS는 공통 목표(알림 전송)는 있지만 인터페이스·구현 관점에서 공유 지식이 없다.
해결: 인터페이스 분리 원칙(ISP)
// 잘못된 예: 공유 지식 없는 메서드를 하나의 인터페이스에 묶음
public interface INotificationProvider {
void SendEmail(Email email);
void SendSMS(PhoneNumber phone, SMS message);
}
// 올바른 예: 각 메서드를 별도 인터페이스로 분리 (거리 확대)
namespace WolfDesk.SCM.Domain.Cases.Notifications {
public interface IEmailNotificationProvider {
void Send(Email email);
}
public interface ISmsNotificationProvider {
void Send(PhoneNumber phone, SMS message);
}
}
공유 지식이 없는 두 메서드는 거리를 늘려야 한다. 인터페이스를 분리하면 클래스의 인터페이스 발자국도 줄어 인지 부하가 추가로 감소한다.
8. 사례 연구 8 — 코드 스멜: 모델 결합을 도메인 서비스로
문제 상황
TrackCustomerEmail 메서드가 Department의 내부 구조(SLAs 딕셔너리)를 직접 접근했다.
// 잘못된 예: 모델 결합 (Department 내부 구조를 SupportCase가 직접 앎)
public void TrackCustomerEmail(Email email, IDepartmentRepository departments) {
var message = Message.FromEmail(email);
this.messages.Append(message);
if (this.AgentAssigned) {
var department = departments.GetDepartmentOf(assignedAgent);
var sla = department.SLAs[this.priority]; // 딕셔너리 구조 직접 접근
this.replyDueDate = DateTime.Now.Add(sla);
}
}
두 가지 문제: (1) SLAs 딕셔너리 구조라는 내부 모델을 알아야 함(모델 결합). (2) 딕셔너리에 키가 없을 때 동작이 불명확(에지 케이스 정책 미정).
해결 단계
1단계 — 메서드 추출로 관심사 분리:
// 관련 없는 로직을 별도 메서드로 추출 (거리 확대)
public void TrackCustomerEmail(Email email, IDepartmentRepository departments) {
var message = Message.FromEmail(email);
this.messages.Append(message);
SetReplyDueDate(departments); // 별도 메서드
}
private void SetReplyDueDate(IDepartmentRepository departments) {
if (this.AgentAssigned) {
var department = departments.GetDepartmentOf(assignedAgent);
var sla = department.SLAs[this.priority];
replyDueDate = DateTime.Now.Add(sla);
}
}
2단계 — 계약 결합으로 전환:
// Department가 GetSLA 메서드를 노출 (구조 대신 메서드 호출)
private void SetReplyDueDate(IDepartmentRepository departments) {
if (this.AgentAssigned) {
var department = departments.GetDepartmentOf(assignedAgent);
var sla = department.GetSLA(this.priority); // 내부 딕셔너리 숨김
replyDueDate = DateTime.Now.Add(sla);
}
}
3단계 — 도메인 서비스로 추출 (완전한 분리):
// SupportCase는 "어떻게 계산하는지" 몰라도 됨
public class SupportCase {
private void SetReplyDueDate(CalcSLA slaService) {
replyDueDate = slaService.CalcDueDate(this.assignedAgent, this.priority);
// 계산 방법(부서 기반인지, 교대 근무 기반인지)은 CalcSLA가 캡슐화
}
}
SupportCase는 "SLA 기한을 계산해야 한다"는 의도만 알면 된다. 통합 강도: 모델 결합 → 계약 결합.
핵심 개념 정리
| 개념 | 한 줄 설명 |
|---|---|
| 비공개/공개 이벤트 분리 | 내부 모델 변경이 외부 소비자에게 전파되지 않도록 통합 전용 이벤트를 별도 정의 |
| 충분히 좋은 통합 | 변동성이 낮으면 모델 결합도 실용적으로 허용 가능 — 세 차원을 함께 읽어야 판단 |
| 수직 슬라이스 | 기술 계층 대신 기능을 조직 단위로 삼아 슬라이스 간 균형, 슬라이스 내 균형 달성 |
| 포트·어댑터 | 종속성 역전으로 도메인이 인프라 지식을 갖지 않게 하고 우발적 변동성을 차단 |
| 집계 경계 | 트랜잭션 결합이 있는 개체만 같은 집계. 나머지는 ID 참조로 거리를 명시적으로 확대 |
| 기능 기준 폴더 | 함께 변경되는 파일은 가까이, 따로 변경되는 파일은 멀리 — 인지 부하 감소 |
| 인터페이스 분리 원칙 | 공유 지식이 없는 메서드는 별도 인터페이스로 분리해 거리를 늘림 |
| 도메인 서비스 | 특정 개체에 귀속하기 어려운 알고리즘을 캡슐화해 모델 결합 → 계약 결합으로 낮춤 |
| 프랙털 자기 유사성 | 마이크로서비스부터 메서드까지 모든 추상화 수준에서 균형 결합 원리가 반복 |
실무 체크리스트
- [ ] 이벤트 소싱 시스템에서 내부 이벤트를 그대로 외부에 게시하고 있지 않은가? 공개 이벤트를 따로 정의했는가?
- [ ] 통합 강도가 높더라도 변동성이 낮다면 "충분히 좋은 통합"인지 먼저 판단했는가? (세 차원 동시 분석)
- [ ] 기능 변경 시 몇 개의 계층/폴더를 수정해야 하는가? 여러 개면 수직 슬라이스를 고려했는가?
- [ ] 비즈니스 로직 코드가 DB 컬럼명·쿼리 방식 같은 인프라 지식을 직접 참조하고 있는가? 포트·어댑터를 적용했는가?
- [ ] ORM 양방향 참조가 허용하는 트랜잭션 범위가 비즈니스 요구사항의 원자 단위와 일치하는가?
- [ ] 클래스 간 객체를 직접 참조하는 코드가 있는가? 트랜잭션 결합이 없다면 ID 참조로 거리를 확대했는가?
- [ ] 폴더 구성이 기술 패턴(Entities, Factories) 기준인가? 기능 책임(SupportCases, Messages) 기준으로 바꿨는가?
- [ ] 같은 인터페이스에 묶인 메서드들이 실제로 공유 지식을 갖고 있는가? 없다면 ISP를 적용했는가?
- [ ] 타 클래스의 필드나 딕셔너리를 직접 접근하는 코드가 있는가? 메서드 또는 도메인 서비스로 캡슐화했는가?
연습문제 5개 (정답: 부록 D)
-
개념. 이벤트 소싱 시스템에서 내부 이벤트를 그대로 외부에 게시하면 어떤 결합 문제가 생기는가? 비공개/공개 이벤트 분리가 결합 세 차원(강도·거리·변동성)에서 각각 무엇을 바꾸는지 설명하라.
-
판단. 다음 두 상황을 보고 각각 "엄격한 계약 설계가 필요한가, 충분히 좋은 통합으로 충분한가"를 판단하고 근거를 설명하라. (a) 핵심 하위 도메인인 결제 서비스가 내부 트랜잭션 이벤트를 보고 서비스에 게시한다. (b) 분기별로만 바뀌는 공휴일 캘린더 서비스가 일정 이벤트를 게시한다.
-
비교. 계층 아키텍처와 수직 슬라이스 아키텍처를 균형 결합 관점에서 비교하라. 각각 어떤 구성요소들이 "높은 강도·가까운 거리" 혹은 "낮은 강도·먼 거리"로 배치되는가? 어느 쪽이 두 수준 모두에서 균형을 달성하는가?
-
리팩터링. 다음 코드에서 결합 문제를 식별하고, 도메인 서비스 패턴을 적용해 리팩터링하라.
csharp public class Order { private void SetDeliveryDate(IWarehouseRepository warehouses) { var wh = warehouses.GetNearestWarehouse(this.customerRegion); var deliveryDays = wh.DeliverySchedule[this.productCategory]; this.deliveryDate = DateTime.Now.AddDays(deliveryDays); } } -
설계. 온라인 쇼핑몰에서 Order(주문), OrderItem(주문 항목), Customer(고객), Product(상품) 4개 클래스가 있다. 어떤 클래스를 같은 집계로 묶고, 어떤 클래스를 ID 참조로만 연결해야 하는가? 비즈니스 원자성을 근거로 설명하라.
최신 동향 (검증 2026-05-21)
- 이벤트 스키마 레지스트리 표준화: 비공개/공개 이벤트 분리 패턴은 AsyncAPI 2.x/3.x 사양과 맞물려 확산 중. Confluent Schema Registry, AWS Glue Schema Registry 등이 계약 결합을 구현하는 표준 인프라로 자리잡고 있다.
- 모듈식 모놀리스 재조명: 수직 슬라이스 + 포트·어댑터 조합을 "모듈식 모놀리스" 패턴으로 채택해 마이크로서비스로의 섣부른 분리 대신 내부 경계를 먼저 정렬하는 접근이 2025~2026년에 주목받고 있다. Feature Slices for ASP.NET, Modular Monolith with DDD 가이드가 이 조합을 지원한다.
- 집계 경계와 이벤트 소싱 조합: DDD 커뮤니티에서 집계 루트가 공개 이벤트를 생성하는 패턴(Aggregate-as-event-source)이 표준으로 자리잡아, 트랜잭션 원자성과 외부 계약 안정성을 동시에 달성하는 방향으로 진화 중이다.
부록 A. 용어 사전
| 한글 용어 | 원문 영문명 | 의미 |
|---|---|---|
| 통합 강도 | Integration Strength | 두 구성요소가 공유하는 지식의 양 |
| 거리 | Distance | 두 구성요소의 위치적 분리 정도 |
| 변동성 | Volatility | 구성요소가 변경되는 빈도 |
| 모델 결합 | Model Coupling | 소비자가 공급자의 내부 도메인 모델 구조를 알아야 하는 결합 |
| 계약 결합 | Contract Coupling | 소비자가 명시적 계약(인터페이스, 공개 스키마)만 알면 되는 결합 |
| 비공개 이벤트 | Private Event | 서비스 내부 수명주기 모델링에 사용되는 이벤트 |
| 공개 이벤트 | Public Event | 외부 통합에 특화된 최소 지식의 이벤트 |
| 수직 슬라이스 | Vertical Slice | 기술 계층 대신 기능을 조직 단위로 삼는 아키텍처 |
| 포트·어댑터 | Ports & Adapters | 도메인 로직을 인프라와 분리하는 아키텍처 패턴. 육각형 아키텍처라고도 함 |
| 집계 | Aggregate | 동일한 트랜잭션 경계를 공유하는 개체 클러스터 |
| 도메인 서비스 | Domain Service | 특정 개체에 귀속하기 어려운 알고리즘을 담당하는 논리적 클래스 |
| 인터페이스 분리 원칙 | ISP (Interface Segregation Principle) | 코드가 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙 |
| 단일 책임 원칙 | SRP (Single Responsibility Principle) | 클래스·모듈은 하나의 책임만 가져야 한다는 원칙 |
| 경계 맥락 | Bounded Context | 도메인 모델이 유효하게 통용되는 명시적 경계 |
| 이벤트 소싱 | Event Sourcing | 상태 변화를 이벤트 연속으로 기록하는 패턴 |
| 우발적 변동성 | Incidental Volatility | 외부 의존성 때문에 원래 안정적이어야 할 구성요소가 변동성을 갖게 되는 현상 |
| WolfDesk | WolfDesk | 이 책 전체의 가상 헬프 데스크 시스템 |
| 충분히 좋은 통합 | Good-Enough Integration | 변동성이 낮아 모델 결합도 실용적으로 허용 가능한 상태 |
| 프랙털 자기 유사성 | Fractal Self-Similarity | 마이크로서비스부터 메서드까지 동일한 균형 결합 원리가 반복되는 특성 |
부록 B. 8 사례 비교표
| 구분 | 사례1 | 사례2 | 사례3 | 사례4 | 사례5 | 사례6 | 사례7 | 사례8 |
|---|---|---|---|---|---|---|---|---|
| 추상화 수준 | 서비스 간 | 서비스 간 | 서비스 내 모듈 | 서비스 내 계층 | 클래스 | 파일/폴더 | 메서드/인터페이스 | 메서드/클래스 |
| 초기 문제 | 모델 결합 + 높은 변동성 | 모델 결합 (낮은 변동성) | 기능 결합 + 계층 분산 | 도메인↔인프라 기능결합 | 트랜잭션 경계 미강제 | 기술 기준 구성 | SRP 위반·공유 지식 없음 | 모델 결합 (딕셔너리 직접 접근) |
| 거리 | 멀다 | 멀다 | 멀다 (계층 간) | 멀다 (계층 간) | 가깝다 | 가깝다 | 가깝다 | 멀다 (다른 슬라이스) |
| 해결 | 공개 이벤트 분리 | 현상 유지 | 수직 슬라이스 | 종속성 역전 | 집계 경계 | 기능 기준 재구성 | ISP 적용 | 도메인 서비스 추출 |
| 강도 변화 | 모델 → 계약 결합 | 변화 없음 | 슬라이스 간 낮아짐 | 기능 → 계약 결합 | 트랜잭션 경계 명확화 | 지역·전역 복잡성 감소 | 인터페이스 분리 | 모델 → 계약 결합 |
부록 C. 추천 참고 자료
| 자료 | 링크 |
|---|---|
| 책 공식 (제이펍) | https://www.jpub.kr/ |
| 원서 — Manning | https://www.manning.com/books/balancing-coupling-in-software-design |
| 마이크로서비스 패턴 (Chris Richardson) | https://microservices.io/patterns/ |
| 도메인 주도 설계 첫걸음 (Vlad Khononov) | https://learning.oreilly.com/library/view/learning-domain-driven-design/9781098100124/ |
| Vertical Slice Architecture (Jimmy Bogard) | https://jimmybogard.com/vertical-slice-architecture/ |
| CQRS Journey (Microsoft) | https://learn.microsoft.com/en-us/previous-versions/msp-n-p/jj554200(v=pandp.10) |
책 다른 장 안내
| 장 | 설명 |
|---|---|
| 7장 | 통합 강도 — 모델·계약·기능 결합 분류 |
| 8장 | 거리 — 구성요소 간 물리적 분리의 영향 |
| 9장 | 변동성 — 핵심·지원·일반 하위 도메인 + 우발적 변동성 |
| 10장 | 균형 결합 — 언제 어떤 결합이 적절한가 |
| 12장 | 복잡성과 프랙털 — 소프트웨어 설계의 자기 유사성 |
부록 D. 연습문제 풀이
-
이벤트 소싱 — 비공개/공개 이벤트 분리의 이유. 내부 이벤트를 그대로 게시하면 소비 서비스가 공급 서비스의 내부 도메인 모델 구조(이벤트 타입·필드명·순서)를 알아야 한다 — 모델 결합. 핵심 하위 도메인이면 변동성이 높아 내부 모델이 자주 바뀐다. 두 서비스가 다른 팀이면 거리도 멀다. "높은 변동성 + 먼 거리 + 모델 결합"은 최악의 조합이다. 공개 이벤트 분리는 통합 강도만 계약 결합으로 낮춘다. 거리와 변동성은 그대로지만 강도가 낮아져 균형이 잡힌다.
-
충분히 좋은 통합 판단. (a) 결제 서비스는 핵심 하위 도메인 — 변동성 높음. 내부 트랜잭션 이벤트는 자주 바뀔 수 있다. 엄격한 계약 설계 필요 — 통합 특화 공개 이벤트를 별도로 정의해야 한다. (b) 공휴일 캘린더는 분기별로만 바뀜 — 변동성 매우 낮음. 내부 모델이 외부로 노출되더라도 변경 전파 빈도가 낮다. 충분히 좋은 통합으로 충분 — 엄격한 계약 설계 비용이 얻는 이점보다 클 수 있다.
-
계층 vs 수직 슬라이스 비교. 계층 아키텍처: 계층 간은 기능 결합(높은 강도) + 먼 거리 — 불균형. 계층 내는 낮은 강도 + 가까운 거리 — 역시 불균형(함께 안 바뀌는 것들이 함께 있음). 수직 슬라이스: 슬라이스 간은 낮은 강도 + 먼 거리 — 균형. 슬라이스 내는 기능 결합(높은 강도) + 가까운 거리 — 균형. 수직 슬라이스가 두 수준 모두에서 균형을 달성한다.
-
Order — 도메인 서비스 리팩터링. 문제: Order가 Warehouse.DeliverySchedule 딕셔너리 구조를 직접 접근 — 모델 결합. Order와 Warehouse는 다른 슬라이스(먼 거리) — 불균형. 해결:
csharp // 1단계: Warehouse에 메서드 노출 (계약 결합) public int GetDeliveryDays(Region region, ProductCategory category) { return DeliverySchedule[category]; // 내부 딕셔너리 캡슐화 } // 2단계: 도메인 서비스로 추출 public class CalcDelivery { public DateTime CalcDeliveryDate(Region region, ProductCategory category) { var wh = warehouseRepo.GetNearest(region); return DateTime.Now.AddDays(wh.GetDeliveryDays(region, category)); } } public class Order { private void SetDeliveryDate(CalcDelivery deliveryService) { this.deliveryDate = deliveryService.CalcDeliveryDate( this.customerRegion, this.productCategory); } } -
집계 경계 설계. 비즈니스 원자성 분석: Order와 OrderItem은 반드시 함께 커밋되어야 한다 — 주문 생성 시 항목도 함께 저장되어야 일관성이 보장된다. Customer와 Product는 Order와 다른 트랜잭션에서 독립적으로 변경된다. 결정: Order + OrderItem을 하나의 집계로, Customer와 Product는 ID 참조만 유지. Order 클래스는 customerId: CustomerId(객체 아닌 ID)와 List<OrderItem>(직접 포함)을 갖는다. 이렇게 하면 고객·상품 정보 탐색 남용을 방지하고 주문-항목 변경의 원자성을 보장한다.
클릭하거나 Space를 눌러 뒤집기